local function bkpt_file_id(filename)
	return filename:gsub("^%w:", function(x)
			return x:lower();
	end);
end;

-- load all breakpoints in the project_configs and opened documents
local function load_breakpoints( self )
	local all_doc = {mgr.get_all_documents()};
	local store_breakpoints = project_configs and (project_configs[".breakpoints"] or {});
	local breakpoints = {};
	if CMAKE_PROJECT_PATH ~= nil then
		for short_name, info in pairs(store_breakpoints) do
			local file_bkps = {};
			local file_id = bkpt_file_id(make_path(CMAKE_PROJECT_PATH, short_name));
			breakpoints[file_id] = file_bkps;
			for line, info in pairs(info) do
				table.insert(file_bkps, line);
			end;
		end;
	end;

	for idx, doc in pairs(all_doc) do
		if doc.doc_type ~= doc_type.hex then
			local bkps = {doc.get_all_breakpoints()};
			local name = bkpt_file_id(doc.file_name);
			if #bkps <=0 then
				breakpoints[name] = nil;
			else
				local file_bkps = {};
				breakpoints[name] = file_bkps;
				for idx, line in pairs(bkps) do
					table.insert(file_bkps, line);
				end;
			end;
		end;
	end;
	self.bkps = {};
	for filename, bpk_info in pairs( breakpoints ) do
		for idx, line in pairs( bpk_info ) do
			if self.bkps[filename] == nil then
				self.bkps[filename] = {};
			end;
			line = math.tointeger(line);
			local bkp_id = self:set_breakpoint_ex( filename, line+1 );
			if bkp_id == 0 then
				-- doc:toggle_breakpoint( line );
				print("Breakpoint disabled:", filename, ":", line);
			else
				print("Breakpoint load:", filename, ":", line+1);
				self.bkps[filename][tostring(line+1)] = bkp_id;
			end;
		end;
	end;
end;

local function remove_breakpoints(filename)
	filename = bkpt_file_id(make_path(filename));
	local doc = mgr:get_document(filename);
	if doc ~= nil then
		-- remove breakpoints in the opened document
		local bkps = {doc.get_all_breakpoints()};
		for i, ln in ipairs(bkps) do
			doc:toggle_breakpoint(ln);
		end;
	end;

	local store_breakpoints = project_configs and project_configs[".breakpoints"];
	if store_breakpoints ~= nil and CMAKE_PROJECT_PATH ~= nil then
		-- remove breakpoints in project_configs
		if filename:sub(0,	#CMAKE_PROJECT_PATH):lower() == CMAKE_PROJECT_PATH:lower() then
			local short_name = filename:sub(2+#CMAKE_PROJECT_PATH);
			short_name = short_name:gsub('\\', '/');
			store_breakpoints[short_name] = nil;
		end;
	end;

	local dbg = edx.dbg;
	if dbg ~= nil and dbg.bkps[filename] ~= nil then
		local bkps = dbg.bkps[filename];
		dbg.bkps[filename] = nil;
		for ln, id in pairs(bkps) do
			dbg:del_breakpoint(id);
		end;
	end;
end;

function list_breakpoints()
	local all_doc = {mgr.get_all_documents()};
	local store_breakpoints = project_configs and (project_configs[".breakpoints"] or {});
	local breakpoints = {};
	if CMAKE_PROJECT_PATH ~= nil then
		for short_name, info in pairs(store_breakpoints) do
			local file_bkps = {};
			local file_id = bkpt_file_id(make_path(CMAKE_PROJECT_PATH, short_name));
			breakpoints[file_id] = file_bkps;
			for line, info in pairs(info) do
				table.insert(file_bkps, line);
			end;
		end;
	end;

	for idx, doc in pairs( all_doc ) do
		local bkps = {doc.get_all_breakpoints()};
		local name = bkpt_file_id(doc.file_name);
		if #bkps <=0 then
			breakpoints[name] = nil;
		else
			local file_bkps = {};
			breakpoints[name] = file_bkps;
			for idx, line in pairs(bkps) do
				table.insert(file_bkps, math.floor(line));
			end;
		end;
	end;

	edx:clear("output");
	print("breakpoints:");
	for name, info in pairs(breakpoints) do
		for i, ln in ipairs(info) do
			print(name, ":", (ln+1), ":");
		end;
	end;
	edx:active("output");
end;

local function dbg_on_exception( self, code, info )
	print(("on exception 0x%08X - %s"):format(code, info));
	if info == "BREAKPOINT" or info:find("SIGTRAP") then
		show_notify(LANG("notify/process_suspended"));
	else
		show_notify(("Exception 0x%08X - %s"):format(code, info));
	end;
	local frames = {self:get_callstack()};
	for idx, frame in ipairs(frames) do
		local src = self.unmap_path(frame.file);
		local line = frame.line;
		if src and utils:file_time(src) and self:select_frame(frame.id) then
			goto success;
		end;
	end;
	self:locate_file();
	::success::
	edx:active( "" );
	menu_bar:switch_debug_menu( "paused" );
	return 1;
end;

local function dbg_on_step( self )
	self.__trace_count = 0;
	print( "step" );
	self:locate_file();
	edx:active( "" );
	menu_bar:switch_debug_menu( "paused" );
	return 1;
end;

local function dbg_on_trace( self )
	local fp = {self:get_src_line()};
	local src = fp[1];
	local line = fp[2];
	if self.__trace_count == nil then
		self.__trace_count = 1;
	else
		self.__trace_count = self.__trace_count + 1;
	end;
	if self.__trace_count >= 10 then
		print( "on trace - too much times" );
	elseif src == nil then
		print( "on trace - again" );
		self:trace();
		return 1;
	elseif self.last_fp[2] == tonumber(line) and self.last_fp[3] == src then
		print( "on trace - again2" );
		self:trace();
		return 1;
	end;
	print( "on trace" );
	self:locate_file();
	edx:active("");
	menu_bar:switch_debug_menu( "paused" );
	return 1;
end;

local function dbg_on_trace_return( self )
	self.__trace_count = 0;
	print( "on trace return" );
	self:locate_file();
	edx:active("");
	menu_bar:switch_debug_menu( "paused" );
	return 1;
end;

local function dbg_on_breakpoint(self, id, file, lineno)
	self.__trace_count = 0;
	file = self.unmap_path(file);
	file = bkpt_file_id(file);
	print(("on breakpoint(%d) %s:%d"):format(id, file, lineno));
	local bkps = self.bkps[file] or {};
	local bkps_id = bkps[tostring(lineno)];
	if bkps_id ~= id then
		print("breakpoint not exists, remove the unknown breakpoint!");
		self:del_breakpoint(id);
	else
		local doc = mgr:get_document(file);
		if doc ~= nil and not doc:has_breakpoint(lineno-1) then
			print("restore missing breakpoint!");
			doc:toggle_breakpoint( lineno-1 );
		end;
	end;

	self:locate_file();
	edx:active("");
	menu_bar:switch_debug_menu( "paused" );
	return 1;
end;

local function dbg_on_load( self )
	self.__trace_count = 0;
	-- edx:clear("output");
	edx:switch_layout( "debug" );
	menu_bar:switch_debug_menu( "debugging" );
	print( "debug session start" );
	if self._post_load_handler then
		self:_post_load_handler();
	end;
	self:load_breakpoints();
	if self._on_load_go then
		self:_on_load_go();
		self._on_load_go = nil;
	else
		self:go();
	end;
	edx:update_dbg_info();
	return 1;
end;

local function dbg_on_terminate( self, code )
	if self.last_fp ~= nil then
		self.last_fp[1].set_dbg_cursor( -1 );
	end;
	edx:clear_dbg_info();
	edx.dbg = nil;
	edx:active( "" );
	print( ("process exit(0x%08X)"):format(code) );
	print( "debug session end" );
	edx:switch_layout( "simple" );
	menu_bar:switch_debug_menu( "stopped" );
	return 1;
end;

local function dbg_on_pause( self )
	self.__trace_count = 0;
	print( "on pause" );
	self:locate_file();
	menu_bar:switch_debug_menu( "paused" );
	return 1;
end;

local function dbg_on_stack_update( self )
	print( "on stack update" );
	self:locate_file();
	return 1;
end;

local function on_thread_switched( self )
	print( "on thread switched" );
	self:locate_file();
	return 1;
end;

local function locate_file(self)
	edx:update_dbg_info();
	local fp = self.fp;
	self.fp = nil;
	if fp == nil then
		fp = {self:get_src_line()};
	end;
	local src = self.unmap_path(fp[1]);
	local line = fp[2];
	if src == nil then
		print( "no source file" );
		return -2;
	end;
	if mgr:open_doc( src ) then
		if self.last_fp ~= nil then
			self.last_fp[1].set_dbg_cursor( -1 );
		end;
		local doc = mgr.current_document;
		doc:set_cursor( tonumber(line)-1, 0 );
		self.last_fp = {doc, tonumber(line), src};
		doc:set_dbg_cursor( tonumber(line)-1 );
		edx:do_cmd( _op.center_cursor_line );
		return 0;
	else
		print( ("can't open source file - %s:%s"):format( tostring(src), tostring(line) ) );
		return -1;
	end;
end;

function get_dbg()
	if edx.dbg == nil then
		local dbg_obj = edx.get_debugger();
		dbg_obj.load_breakpoints = load_breakpoints;
		dbg_obj.on_exception = dbg_on_exception;
		dbg_obj.on_step = dbg_on_step;
		dbg_obj.on_trace = dbg_on_trace;
		dbg_obj.on_trace_return = dbg_on_trace_return;
		dbg_obj.on_breakpoint = dbg_on_breakpoint;
		dbg_obj.on_load = dbg_on_load;
		dbg_obj.on_terminate = dbg_on_terminate;
		dbg_obj.on_pause = dbg_on_pause;
		dbg_obj.on_stack_update = dbg_on_stack_update;
		dbg_obj.on_thread_switched = on_thread_switched;
		dbg_obj.locate_file = locate_file;
		dbg_obj.map_path = function(path) return path; end;
		dbg_obj.unmap_path = function(path) return path; end;
		dbg_obj.set_breakpoint_ex = function(self, file, line)
			return self:set_breakpoint(self.map_path(file), line);
		end;
		dbg_obj.on_notify = function(self, event, info)
			-- print("on_notify:", event, " ", info);
			if event == "create thread" or event == "exit thread" then
				edx:update_dbg_info_ex("threads");
			elseif event == "load module" or event == "unload module" then
				edx:update_dbg_info_ex("modules");
			end;
		end;
		edx.dbg = dbg_obj;
		return dbg_obj;
	end;
	return edx.dbg;
end;

function clear_dbg()
	local dbg_obj = edx.dbg;
	if dbg_obj == nil then
		return;
	end;
	if dbg_obj.last_fp ~= nil then
		dbg_obj.last_fp[1].set_dbg_cursor( -1 );
	end;
	edx.dbg = nil;
	print( "debug session end" );
end;

function load_program()
	reload_cmake_project_settings();
	local cmd = find_build_command();
	local toolset = cmd and cmd.toolset;
	local dbg_target = _G["dbg_target"];
	local dbg_environment = nil;
	local dbg_working_path = nil;
	local dbg_argument = nil;
	local dbg_type = _G["dbg_type"] or "windbg";
	if cmd and cmd.target_output then
		dbg_type = cmd.debug_type;
		if dbg_type == "openocd" then
			dbg_type = "gdb/mi";
		end;
		edx:select_debugger(dbg_type);
		edx.dbg = nil;
		dbg_target = cmd.target_output;
		dbg_argument = cmd.argument;
		dbg_working_path = cmd.cwd;
		dbg_environment = cmd.environment;
	end
	local map_path = (toolset and toolset.map_path) or function(path) return path; end;
	local unmap_path = (toolset and toolset.unmap_path) or function(path) return path; end;
	print("debug:", dbg_type, ":", dbg_target);
	local dbg = get_dbg();

	dbg.map_path = map_path;
	dbg.unmap_path = unmap_path;
	dbg.dbg_type = dbg_type;
	dbg.beautifier = edx.get_debugger_beautifier(dbg_type);
	if dbg_environment then
		for key, value in pairs(dbg_environment) do
			dbg:set_environment(key, value);
		end;
	end;
	if dbg_working_path then
		if dbg_working_path:sub(-1) == "\\" and #dbg_working_path > 3 then
			dbg_working_path = dbg_working_path:sub(1,-2);
		end;
		if toolset and toolset.wsl then
			dbg_working_path = map_path(dbg_working_path);
			dbg:set_working_path(dbg_working_path);
		elseif utils:file_time(dbg_working_path) or dbg_working_path:match([[^%w:\$]]) then
			dbg_working_path = map_path(dbg_working_path);
			dbg:set_working_path(dbg_working_path);
		else
			print("ERROR: debugee working directory not exists! ", dbg_working_path);
			return false;
		end;
	end;
	if dbg_argument then
		if type(dbg_argument) == "table" then
			dbg_argument = table.concat(dbg_argument, " ");
		end;
		dbg:set_args(dbg_argument);
	end;
	if dbg_type:match("gdb/") then
		dbg._post_load_handler = function()
			if cmd then
				print("debug target:", cmd.target_output, " ", cmd.argument);
				if cmd.cwd then
					print("debug cwd:", cmd.cwd);
				end;
			end;
			if toolset and toolset.wsl then
				dbg:set_param("ENABLE_WSL_CONSOLE", toolset.wsl);
			end;
			print("load gcc-pretty-print...");
			if toolset and toolset.tools and toolset.tools["gcc-pretty-print"] then
				local pretty_print_path;
				if toolset.get_pretty_print_path then
					dbg_call(function()
							pretty_print_path = toolset:get_pretty_print_path();
					end, "");
				else
					pretty_print_path = toolset.tools["gcc-pretty-print"];
				end;

				pretty_print_path = pretty_print_path:gsub("\\", "/");
				print("gcc-pretty-print:", pretty_print_path);

				dbg:exec(
					"python\n"..
					"import sys\n"..
					"sys.path.insert(0, '"..pretty_print_path.."')\n"..
					"from libstdcxx.v6.printers import register_libstdcxx_printers\n"..
					"register_libstdcxx_printers(None)\n"..
					"end\n"
				);
				dbg:exec("-enable-pretty-printing\n");
			else
				print("gcc-pretty-print not found");
			end;
		end;
		if toolset and toolset.tools and toolset.tools.gdb then
			dbg:set_param("GDB_PATH", toolset.tools.gdb);
		elseif GDB_PATHS and GDB_PATHS[1] then
			dbg:set_param("GDB_PATH", GDB_PATHS[1]);
		end;
		if toolset.target == "avr" then
			dbg:set_param("GDB_SERVER", "run_avr -g ${GDB_PORT} ${TARGET_PATH}");
		elseif toolset["type"] == "esp32-idf" then
			local openocd_root = toolset.envs["OPENOCD_ROOT"];
			local openocd_scripts = toolset.envs["OPENOCD_SCRIPTS"];
			local openocd_path = make_path(openocd_root, "openocd.exe");
			local esp_target = toolset.target;
			local debug_adapter = menu_bar.get_cmake_debug_adapter() or "esp_usb_jtag";
			local openocd = "\""..openocd_path.."\"";
			openocd = openocd.." --search \""..openocd_scripts.. "\"";
			openocd = openocd.." --file interface/"..debug_adapter.. ".cfg";
			if debug_adapter == "jlink" then
				openocd = openocd.." -c \"adapter speed 2000\"";
			end;
			openocd = openocd.." --file target/"..esp_target.. ".cfg";
			openocd = openocd.." -c \"gdb_port ${GDB_PORT}\"";
			openocd = openocd.." -c \"telnet_port ${TELNET_PORT}\"";

			dbg:set_param("OPENOCD", openocd);
			dbg:set_param("MAIN", "");
			dbg:set_param("GDB_INIT", make_path(cmd.build_path, ".gdbinit"));
			dbg:set_param("GDB_INIT_PRE", "set remotetimeout 60000\n");
			dbg._on_load_go = function(self)
				self:exec("monitor reset halt\n");
				self:go();
			end;
		end;
	end;
	if toolset then
		if toolset.wsl then
			dbg_target = string.format("wsl://[%s]%s", toolset.wsl, dbg_target);
		elseif toolset.remote then
			dbg_target = string.format("%s%s", toolset.remote, dbg_target);
		end;
	end;
	if not dbg_target then
		print("debugee not specified!");
		return;
	end;
	if not dbg:load(dbg_target) then
		if utils:file_time(dbg_target) then
			show_notify(LANG("notify/debug/invalid_target") .. "\n" .. dbg_target);
		else
			show_notify(LANG("notify/debug/target_not_exists") .. "\n" .. dbg_target);
		end;
	end;
end;

function debug_program(target, dbg_type, dbg_argument, dbg_working_path, dbg_environment)
	if dbg_type == nil then
		dbg_type = "windbg";
	end;
	edx:select_debugger(dbg_type);
	local dbg = get_dbg();
	if dbg.is_running() then
		print("debugger is running");
		return;
	end;
	if dbg_environment then
		for key, value in pairs(dbg_environment) do
			dbg:set_environment(key, value);
		end;
	end;
	if dbg_working_path then
		if dbg_working_path:sub(-1) == "\\" and #dbg_working_path > 3 then
			dbg_working_path = dbg_working_path:sub(1,-2);
		end;
		dbg:set_working_path(dbg_working_path);
	end;
	if dbg_argument then
		dbg:set_args(dbg_argument);
	end;
	dbg.map_path = function(p) return p; end;
	dbg.unmap_path = function(p) return p; end;
	dbg.dbg_type = dbg_type;
	dbg.beautifier = edx.get_debugger_beautifier(dbg_type);
	dbg:load(target);
end;

global_cmd_map[_op.debug_start] = function(self,id)
	print( "run" );
	local dbg_obj = get_dbg();
	if dbg_obj:is_running() then
		if dbg_obj.last_fp ~= nil then
			dbg_obj.last_fp[1].set_dbg_cursor( -1 );
		end;
		menu_bar:switch_debug_menu( "debugging" );
		dbg_obj:go();
	else
		load_program();
	end;
	return 1;
end;

global_cmd_map[_op.debug_terminate] = function(self,id)
	print( "terminate" );
	local dbg_obj = edx.dbg;
	if dbg_obj ~= nil and dbg_obj:is_running() then
		dbg_obj:terminate();
	end;
	return 1;
end;

global_cmd_map[_op.debug_step_over] = function(self,id)
	print( "step" );
	local dbg_obj = get_dbg();
	if dbg_obj:is_running() then
		menu_bar:switch_debug_menu( "debugging" );
		dbg_obj:step();
	else
		load_program();
	end;
	return 1;
end;

global_cmd_map[_op.debug_step_in] = function(self,id)
	print( "trace into" );
	local dbg_obj = get_dbg();
	if dbg_obj:is_running() then
		menu_bar:switch_debug_menu( "debugging" );
		dbg_obj:trace();
	else
		load_program();
	end;
	return 1;
end;

global_cmd_map[_op.debug_step_out] = function(self,id)
	print( "trace return" );
	local dbg_obj = get_dbg();
	if dbg_obj:is_running() then
		menu_bar:switch_debug_menu( "debugging" );
		dbg_obj:trace_return();
	else
		load_program();
	end;
	return 1;
end;

global_cmd_map[_op.debug_toggle_break_point] = function(self,id)
	print( "toggle breakpoint" );
	local dbg_obj = edx.dbg;
	local doc = mgr.current_document;
	local name = bkpt_file_id(doc.file_name);
	local line = doc.cursor_line;

	if dbg_obj == nil then
		doc:toggle_breakpoint( line );
		return;
	end;

	local bkps = dbg_obj.bkps;
	if bkps == nil then
		bkps = {};
		dbg_obj.bkps = bkps;
	end;
	if bkps[name] == nil then
		bkps[name] = {};
	end;

	if doc:toggle_breakpoint( line ) then
		local bkp_id = dbg_obj:set_breakpoint_ex( name, line+1 );
		if bkp_id == 0 then
			print( "Can't set breakpoint!" );
			doc:toggle_breakpoint( line );
		else
			bkps[name][tostring(line+1)] = bkp_id;
		end;
	elseif bkps[name][tostring(line+1)] ~= nil then
		if dbg_obj:del_breakpoint( bkps[name][tostring(line+1)] ) then
			bkps[name][tostring(line+1)] = nil;
		else
			print( "Can't delete breakpoint!" );
			doc:toggle_breakpoint( line );
		end;
	end;
	return 1;
end;

global_cmd_map[_op.debug_break] = function(self,id)
	print( "pause" );
	local dbg_obj = edx.dbg;
	if dbg_obj ~= nil and dbg_obj:is_running() then
		dbg_obj:pause();
	end;
	return 1;
end;

global_cmd_map[_op.debug_jump_to] = function(self,id)
	print( "jump" );
	local dbg_obj = edx.dbg;
	local dbg_obj = edx.dbg;
	local doc = mgr.current_document;
	local name = doc.file_name;
	local line = doc.cursor_line;
	if dbg_obj ~= nil and dbg_obj:is_running() then
		dbg_obj:jump(dbg_obj.map_path(name), line);
	end;
	return 1;
end;

menu_bar:switch_debug_menu( "stopped" );
print( "load debugger binding ok!" );

-- 加载 debugger beautifiers
dbg_call(function() require "windbg_beautifier" end, "loading windbg beautifier failed!");
-- dbg_call(function() require "gdbmi_beautifier" end, "loading gdbmi beautifier failed!");
